package com.blue.base.shiro.util;

import cn.hutool.http.HttpStatus;
import com.alibaba.fastjson.JSON;
import com.blue.base.common.utils.thread.ThreadPoolUtils;
import com.blue.base.common.exception.ShiroUnAuthorizedException;
import com.blue.base.shiro.api.ShiroAuthService;
import com.blue.base.shiro.bean.Role;
import com.blue.base.shiro.constants.ShiroConfigBean;
import com.blue.base.shiro.realms.AuthRealms;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * @author liulei
 * @version 1.0
 */
@Slf4j
@Component
@ConditionalOnProperty(value = "sys.auth.enabled")
public class ShiroUtil implements ApplicationContextAware {

    /**
     * 分布式sessionID
     */
    public static final String SESSION_ACCOUNT_ID_KEY = "sessionid";
    /**
     * 分布式自定义sessionAccountName
     */
    public static final String SESSION_ACCOUNT_NAME_KEY = "accountName";

    @Autowired
    private ShiroConfigBean configBean;
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private ShiroAuthService shiroAuthService;
    @Autowired
    private RedisSessionDAO redisSessionDAO;

    /**
     * 为避免循环依赖，通过context获取AuthRealms对象
     */
    private ApplicationContext applicationContext;

    /**
     * 获取当前登录用户的账户名称
     */
    public String getLoginAccountName() {
        Subject subject = SecurityUtils.getSubject();
        if (Objects.isNull(subject.getPrincipal())) {
            throw new ShiroUnAuthorizedException(HttpStatus.HTTP_UNAUTHORIZED, "认证失败，尚未登录");
        }
        return String.valueOf(subject.getPrincipal());
    }

    /**
     * 初始化登录操作
     */
    public void login(String account, String password) {
        Subject subject = SecurityUtils.getSubject();
        if (Objects.nonNull(subject.getPrincipal())) {
            clearCurrentUserAuthCache();
        }
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(account, password);
        subject.login(usernamePasswordToken);
        subject.getSession().setAttribute(SESSION_ACCOUNT_ID_KEY, subject.getSession().getId());
        subject.getSession().setAttribute(SESSION_ACCOUNT_NAME_KEY, account);
    }

    /**
     * 退出动作
     */
    public void logOut() {
        String accountName = this.getLoginAccountName();
        SecurityUtils.getSubject().logout();
        if (!StringUtils.isEmpty(accountName)) {
            log.info("清除已登录用户【{}】信息", accountName);
            this.redisTemplate.delete(configBean.getAuthKeyPrefix() + accountName);
        }
    }

    /**
     * 手动清除account集合账户的缓存
     */
    public void clearCurrentUserAuthCache() {
        //清除account用户授权缓存
        Subject subject = SecurityUtils.getSubject();
        String account = (String) subject.getPrincipal();
        AuthRealms authRealms = applicationContext.getBean(AuthRealms.class);
        Cache<Object, AuthorizationInfo> cache = authRealms.getAuthorizationCache();
        if (cache != null) {
            cache.remove(subject.getPrincipal());
        }
        log.info("删除当前登录用户权限缓存【{}】", account);
        redisTemplate.delete(configBean.getAuthKeyPrefix() + account);
    }

    /**
     * 强制删除掉shiro的redis权限，迫使用户在执行后台请求时，重新调用接口
     *
     * @see AuthRealms-doGetAuthorizationInfo(PrincipalCollection)
     */
    public void removeUserAuthCacheByAccountNames(List<String> accounts) {
        AuthRealms authRealms = applicationContext.getBean(AuthRealms.class);
        Cache<Object, AuthorizationInfo> cache = authRealms.getAuthorizationCache();
        if (cache != null) {
            for (String account : accounts) {
                cache.remove(account);
                log.info("只删除当前登录相关用户权限缓存【{}】，保留在线/授权状态", account);
                redisTemplate.delete(configBean.getAuthKeyPrefix() + account);
            }
        }
    }

    /**
     * 强制踢掉在线用户
     */
    public void kickOutOnLineUser(List<String> accounts) {
        AuthRealms authRealms = applicationContext.getBean(AuthRealms.class);
        Cache<Object, AuthenticationInfo> authecCache = authRealms.getAuthenticationCache();
        Cache<Object, AuthorizationInfo> authorCache = authRealms.getAuthorizationCache();
        if (authecCache != null) {
            // redisSessionDAO.getActiveSessions操作较为费时，启用线程异步操作【原因：底层实现redis.get(*)】
            ThreadPoolUtils.execute(() -> {
                for (String account : accounts) {
                    authecCache.remove(account);
                    authorCache.remove(account);
                    for (Session session : redisSessionDAO.getActiveSessions()) {
                        if (session.getAttribute(SESSION_ACCOUNT_NAME_KEY).toString().equals(account)) {
                            //删除分布式session缓存
                            redisSessionDAO.delete(session);
                            log.info("删除对应用户的所有缓存信息【{}】", account);
                            redisTemplate.delete(configBean.getAuthKeyPrefix() + account);
                        }
                    }
                }
            });
        }
    }

    /**
     * 获取所有已在线的用户信息【sessionId-account】
     */
    public List<String> getLoginAccounts() {
        List<String> result = Lists.newArrayList();
        Collection<Session> sessions = redisSessionDAO.getActiveSessions();
        for (Session session : sessions) {
            String sessionId = session.getAttribute(SESSION_ACCOUNT_ID_KEY).toString();
            String account = session.getAttribute(SESSION_ACCOUNT_NAME_KEY).toString();
            log.info("account-sessionId:{}-{}", sessionId, account);
            result.add(account + "-" + sessionId);
        }
        return result;
    }

    /**
     * 获取登录用户的角色及权限信息
     *
     * @return 角色、权限
     */
    public List<Role> getLoginAccount() {
        String account = this.getLoginAccountName();
        Objects.requireNonNull(account, "用户信息一定不为空");
        List<Role> roleInfo = shiroAuthService.getRoleAndPermissionByAccount(account);
        String redisKey = configBean.getAuthKeyPrefix() + account;
        redisTemplate.opsForValue().set(redisKey, JSON.toJSONString(roleInfo));
        redisTemplate.expire(redisKey, ShiroConfigBean.EXPIRE_SECONDS, TimeUnit.SECONDS);
        return roleInfo;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

}
